/*
 * Giffel, Android application
 *
 * Copyright (C) 2011  Mid Sweden University
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package se.appening;

import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import android.location.Location;

public class LiveCringleGetter implements CringleGetter {
    
    private static final double LATPERMETER = 0.00001;
    private static final double ONEKILOMETERINLAT = 1000 * LATPERMETER;
    private static final double TENKILOMETERINLAT = 10 * ONEKILOMETERINLAT;

    private static final String requestUrlPrefix = "http://kulturarvsdata.se/ksamsok/api?";
    private static final String requestUrlForTotalHits = "method=statisticSearch&index=mediaType=*&x-api=test&query=boundingBox=/WGS84%20";
    private static final String requestUrlForRecords = "method=search&hitsPerPage=10&x-api=test&query=boundingBox=/WGS84%20";
    protected int nrCringles = 8;

    private SubjectEvent cringlesUpdatedEvent = new SubjectEvent("CringlesUpdated");

    @Override
    public int getNrCringles() {
        return nrCringles;
    }

    @Override
    public void setNrCringles(int nrCringles) {
        this.nrCringles = nrCringles;
    }

    private final List<Entry> entries = new ArrayList<Entry>();
    private Location currentPosition;

    /*
     * (non-Javadoc)
     * 
     * @see se.appening.CringleGetter#updatePosition(android.location.Location)
     */
    public void updatePosition(Location position) {
        currentPosition = position;
        getCringlesFor(position);
        cringlesUpdatedEvent.signal(new CringlesList(getEntries()));
    }

    private void getCringlesFor(Location position) {
        double boxSize = ONEKILOMETERINLAT;
        double lat = position.getLatitude();
        double lng = position.getLongitude();

        int numberOfResults = 0;
        String result;
        do {
            String requestUrl = formRequestUrlForTotalHits(boxSize, lat, lng);
            result = performHttpRequest(requestUrl);
            numberOfResults = retrieveAndParseTotalHits(result);

            boxSize = boxSize + ONEKILOMETERINLAT;
        } while (numberOfResults < nrCringles && boxSize <= TENKILOMETERINLAT);

        String requestUrl = formRequestUrlForRecords(boxSize, lat, lng);
        result = performHttpRequest(requestUrl);
        retrieveAndParseResult(result);
    }

    private String formRequestUrlForTotalHits(double boxSize, double lat,
            double lng) {
        return requestUrlPrefix + requestUrlForTotalHits + formBoundingBoxString(boxSize, lat, lng);
    }

    private String formRequestUrlForRecords(double boxSize, double lat,
            double lng) {
        return requestUrlPrefix + requestUrlForRecords + formBoundingBoxString(boxSize, lat, lng);
    }

    private String formBoundingBoxString(double boxSize, double lat, double lng) {
        double west = lng - boxSize;
        double south = lat - boxSize;
        double east = lng + boxSize;
        double north = lat + boxSize;
        return String.format("%%22%f%%20%f%%20%f%%20%f%%22", west, south, east, north);
    }

    private String performHttpRequest(String requestUrl) {
        HttpClient httpclient = new DefaultHttpClient();
        String responseBody = "";
        try {
            URI uri = new URI(requestUrl);
            HttpGet httpget = new HttpGet(uri);
            ResponseHandler<String> responseHandler = new BasicResponseHandler();
            responseBody = httpclient.execute(httpget, responseHandler);
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } finally {
            httpclient.getConnectionManager().shutdown();
        }
        return responseBody;
    }

    private int retrieveAndParseTotalHits(String uri) {
        int returnValue = -1;
        Document xmlDocument = getXmlDocument(uri);
        returnValue = parseTotalHits(xmlDocument);
        return returnValue;
    }

    private Document getXmlDocument(String result) {
        try {
            DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = domFactory.newDocumentBuilder();
            
            InputSource is = new InputSource(new StringReader(result));
            return docBuilder.parse(is);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private int parseTotalHits(Document xmlDocument) {
        int totalHits = -1;
        Element element = xmlDocument.getDocumentElement();
        if (element.getNodeName().contentEquals("result")) {
            NodeList termElements = element.getElementsByTagName("term");
            Element term = null;
            for (int i = 0; i < termElements.getLength(); i++) {
                if (nodeHasPlainContent((Element) termElements.item(i))) {
                    term = (Element) termElements.item(i);
                    break;
                }
            }
            if (term != null) {
                totalHits = getRecords(term);
            }
        }

        return totalHits;
    }

    private boolean nodeHasPlainContent(Element term) {
        Element indexFields = getSingleElementFrom(term, "indexFields");
        Element value = getSingleElementFrom(indexFields, "value");
        return "text/plain".contentEquals(getTextFrom(value, ""));
    }

    private String getTextFrom(Element element, String defaultValue) {
        String result = defaultValue;
        if (element != null) {
            NodeList nodes = element.getChildNodes();
            result = nodes.item(0).getNodeValue();
        }
        return result;
    }

    private int getRecords(Element term) {
        NodeList list = term.getElementsByTagName("records");
        String value = getTextFrom((Element) list.item(0), "-2");
        int records = Integer.parseInt(value);
        return records;
    }

    private void retrieveAndParseResult(String string) {
        synchronized (entries) {
            entries.clear();

            Document xmlDocument = getXmlDocument(string);

            NodeList records = parseResultList(xmlDocument);
            if (records != null) {
                for (int i = 0; i < records.getLength(); i++) {
                    Entry e = parseEntry((Element) records.item(i));
                    entries.add(e);
                }
            }
        }
    }

    private NodeList parseResultList(Document xmlDocument) {
        Element element = xmlDocument.getDocumentElement();
        if (element.getNodeName().contentEquals("result")) {
            Element records = getSingleElementFrom(element, "records");
            return records.getElementsByTagName("record");
        }
        return null;
    }

    private Element getSingleElementFrom(Element element, String name) {
        return (Element) element.getElementsByTagName(name).item(0);
    }

    private Entry parseEntry(Element record) {
        Entry e = null;
        Element rdf = getSingleElementFrom(record, "rdf:RDF");
        NodeList descriptions = rdf.getElementsByTagName("rdf:Description");
        Element presentation = findPresentationElement(descriptions);
        if (presentation != null) {
            Element item = (Element) presentation.getFirstChild();
            Element id = getSingleElementFrom(item, "pres:id");
            Element description = getSingleElementFrom(item, "pres:description");
            Element type = getSingleElementFrom(item, "pres:type");
            Element location = getLocationElement(item);
            Element title = getTitleElement(item);
            
            e = new Entry();
            e.id = getTextFrom(id, "");
            e.description = getTextFrom(description, "NO DESCRIPTION");
            e.type = getTextFrom(type, "NO TYPE");
            e.title = getTextFrom(title, "NO TITLE");
            
            String pos[] = getTextFrom(location, "0,0").split(",");
            e.location = new Location("Cringle");
            e.location.setLatitude(Location.convert(pos[1]));
            e.location.setLongitude(Location.convert(pos[0]));
            e.distance = currentPosition.distanceTo(e.location);
            
            Element thumbnail = findThumbnailElement(descriptions);
            if (thumbnail != null) {
                e.imageUrl = getTextFrom(thumbnail, "");
            }
        }

        return e;
    }

    private Element findThumbnailElement(NodeList descriptions) {
        Element thumbnail = null;
        for (int i = 0; i < descriptions.getLength(); i++) {
            Element description = (Element) descriptions.item(i);
            thumbnail = getSingleElementFrom(description, "ns5:thumbnailSource");
            if (thumbnail != null)
                break;
        }
        return thumbnail;
    }

    private Element getLocationElement(Element presentation) {
        Element where = getSingleElementFrom(presentation, "georss:where");
        Element point = getSingleElementFrom(where, "gml:Point");
        Element coordinates = getSingleElementFrom(point, "gml:coordinates");
        return coordinates;
    }

    private Element getTitleElement(Element presentation) {
        Element parent = (Element) presentation.getParentNode().getParentNode();
        Element itemLabel = getSingleElementFrom(parent, "ns5:itemLabel");
        return itemLabel;
    }

    private Element findPresentationElement(NodeList descriptions) {
        Element presentation = null;
        for (int i = 0; i < descriptions.getLength(); i++) {
            Element description = (Element) descriptions.item(i);
            presentation = getSingleElementFrom(description, "ns5:presentation");
            if (presentation != null)
                break;
        }
        return presentation;
    }

    /*
     * (non-Javadoc)
     * 
     * @see se.appening.CringleGetter#getEntries()
     */
    public Entry[] getEntries() {
        Entry[] eArray;
        synchronized (entries) {
            eArray = entries.toArray(new Entry[0]);
        }
        Arrays.sort(eArray, new Comparator<Entry>() {
            public int compare(Entry object1, Entry object2) {
                float diff = object1.distance - object2.distance;
                return diff < 0 ? -1 : diff > 0 ? 1 : 0;
            }
        });
        return eArray;
    }

    @Override
    public SubjectEvent cringlesUpdatedEvent() {
        return cringlesUpdatedEvent;
    }

}
